/*   gameoflife - Conway's Game of Life implemented w/ SDL
 *   Copyright (C) 2012 william.a.west@gmail.com
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License along
 *   with this program; if not, write to the Free Software Foundation, Inc.,
 *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include "SDL/SDL.h"
#include "SDL/SDL_Draw.h"

#define WIDTH 1680 
#define HEIGHT 1050
#define BPP 8
#define CELLPX 2 /* use even divisor of WIDTH and HEIGHT */

int** cells;
int gridlines = 1;
int runstate = 0;

SDL_Surface* set_surface(void);
void loop(SDL_Surface*);
void draw_screen(SDL_Surface*, int);
void toggle_cell(SDL_Surface *, int, int);
void spawn_n_cull();
int** new_generation();

int main( int argc, char* args[] ) {
    //
    // allocate n array of arrays representing cells. 
    int n;
    cells = (int**)calloc(WIDTH/CELLPX, sizeof(int*));
    for(n=0; n<WIDTH/CELLPX; n++) 
        cells[n] = (int*)calloc(HEIGHT/CELLPX, sizeof(int));

    //
    // need these to set up display and event loop
    SDL_Surface *screen;
    //SDL_Event event;
    
    // init SDL
    if ( SDL_Init( SDL_INIT_EVERYTHING ) < 0 ) {
		fprintf(stderr, "Unable to init SDL: %s\n", SDL_GetError());
		exit(1);
	}

    screen = set_surface();
	loop(screen);

    // gracefully quit SDL
    SDL_Quit();
    return 0;    
}

SDL_Surface* set_surface() { 
	SDL_Surface *screen;

    screen = SDL_SetVideoMode(WIDTH, HEIGHT, BPP,
                 SDL_HWSURFACE|SDL_DOUBLEBUF|SDL_ANYFORMAT|SDL_FULLSCREEN);
	if ( screen == NULL ) {
        fprintf(stderr, "Unable to set video mode: %s\n", SDL_GetError());
        exit(1);
	}
    return screen;
}

void loop(SDL_Surface* screen) {
    int mouseclick = 0;
    int h = 0;
    SDL_Event event;
    int delay=25000;

    while(1) {
        // spawn next generation if we're running the sim
        if(runstate) {
            spawn_n_cull();
            usleep(delay);
        }
        draw_screen(screen,h++);
        while(SDL_PollEvent(&event)) {      
            switch (event.type) {
                case SDL_MOUSEBUTTONDOWN:
                    mouseclick = 1;
                    toggle_cell(screen, event.button.x, event.button.y);
                    break;
                case SDL_KEYDOWN:
                    // toggle running the sim
                    switch(event.key.keysym.sym) {
                        case SDLK_ESCAPE:
                            exit(0);
                            break;
                        case SDLK_q:
                            exit(0);
                            break;
                        case SDLK_SPACE:
                            runstate = !runstate;
                            break;
                        case SDLK_BACKSPACE:
                            clear_cells();
                            runstate = 0;
                            break;
                        case SDLK_g:
                            gridlines = !gridlines;
                            break;
                        case SDLK_r:
                            randomize_cells();
                            break;
                        case SDLK_DOWN:
                            delay += 100000;
                            if(delay >= 5000000) delay = 5000000;
                            break;
                        case SDLK_UP:
                            delay -= 100000;
                            if(delay <= 0) delay = 0;
                            break;
                    }
                    break;
                case SDL_QUIT:
                    exit(0);
            }
        }
    }
}

void draw_screen(SDL_Surface* screen, int h) { 
    int x; int y; int ytimesw;
    Uint32 c_black = SDL_MapRGB(screen->format, 0,0,0);
    Uint32 c_white = SDL_MapRGB(screen->format, 255,255,255);
  
    if(SDL_MUSTLOCK(screen)) {
        if(SDL_LockSurface(screen) < 0) return;
    }

    // scale to the screen. fill with white
    Draw_FillRect(screen, 0, 0, screen->w, screen->h,
                  SDL_MapRGB(screen->format, 255,255,255));

    // draw grid
    // height - # lines is screen height / GRIDY (# rows)
    if(gridlines) {
        for(y=0; y<screen->h; y+=CELLPX) {
            Draw_HLine(screen, 0, y, (screen->w)-1, c_black);
        }
        for(x=0; x<screen->w; x+=CELLPX) {
            Draw_VLine(screen, x, 0, (screen->h)-1, c_black);
        }
    }

    // iterate through cells and based on value:
    // 0: draw an inside fill square (white)
    // 1: draw a inside square (black)
    for(x=0; x<WIDTH/CELLPX; x++) {
        for(y=0; y<HEIGHT/CELLPX; y++) {
            if(cells[x][y]) {
            Draw_FillCircle(screen, 
                          (x*CELLPX)+CELLPX/2, (y*CELLPX)+CELLPX/2,
                          CELLPX/2, 
                          SDL_MapRGB(screen->format, 0,0,0));
            } else {
            Draw_FillCircle(screen, 
                          (x*CELLPX)+CELLPX/2, (y*CELLPX)+CELLPX/2,
                          CELLPX/2,
                          SDL_MapRGB(screen->format, 255,255,255));
            }
        }
    }

    if(SDL_MUSTLOCK(screen)) SDL_UnlockSurface(screen);
  
    SDL_Flip(screen); 
}

//
// toggle the cell that includes point (x,y)
void toggle_cell(SDL_Surface *screen, int x, int y) {

    cells[x/CELLPX][y/CELLPX] = !cells[x/CELLPX][y/CELLPX];
    SDL_Flip(screen); 
}

// could threadify
void spawn_n_cull() {
    int sum, x, y, alive;
    int** newcells;
    int** oldcells;
    
    // allocate new generation 
    newcells = new_generation();

    // for each cell
    for(x=0; x<WIDTH/CELLPX; x++) {
        for(y=0; y<HEIGHT/CELLPX; y++) {
            // sum up neighbor states
            sum = sum_neighbors(x, y);
            alive = cells[x][y];
            // if alive and 2<=sum<=3, live
            if(alive && (sum == 2 || sum == 3)) newcells[x][y] = 1;
            // if alive and sum>3, die 
            if(alive && sum > 3) newcells[x][y] = 0;
            // if alive and sum<2, die 
            if(alive && sum < 2) newcells[x][y] = 0;
            // if dead and sum==3, live
            if(!alive && sum == 3) newcells[x][y] = 1;
        }
    }

    // replace old generation with the new one; free old one.
    oldcells = (int**)cells;
    cells = newcells;
    free_generation(oldcells);
}

int sum_neighbors(int x, int y) {
    int sum,xneg,xpos,yneg,ypos;
   
    // use modulo to wrap on edges
    xneg = mod((x-1), (WIDTH/CELLPX));
    xpos = mod((x+1), (WIDTH/CELLPX));
    yneg = mod((y-1), (HEIGHT/CELLPX));
    ypos = mod((y+1), (HEIGHT/CELLPX));

    sum = cells[xneg][yneg] 
          + cells[x][yneg]
          + cells[xpos][yneg]
          + cells[xneg][ypos]
          + cells[x][ypos]
          + cells[xpos][ypos]
          + cells[xneg][y]
          + cells[xpos][y];

    return sum;
}

//
// gives correct modulus for negative dividends
int mod (int dividend, int divisor) {
    int mod;
    mod = dividend % divisor;
    if(mod < 0)
        mod += divisor;
    return mod;
}

int** new_generation() {
    int** new;
    //
    // allocate n array of arrays representing cells. 
    int n;
    new = (int**)calloc(WIDTH/CELLPX, sizeof(int*));
    for(n=0; n<WIDTH/CELLPX; n++) 
        new[n] = (int*)calloc(HEIGHT/CELLPX, sizeof(int));
    return new;
}

free_generation(int** oldcells) {
    int n;
    for(n=0; n<WIDTH/CELLPX; n++) {
        free(oldcells[n]);
    }
    free(oldcells);
}

randomize_cells() {
    int x,y;
    srandom(time(NULL));

    for(x=0; x<WIDTH/CELLPX; x+=(random() % 5)) {
        for(y=0; y<HEIGHT/CELLPX; y+=(random() % 7)) {
            cells[x][y] = random() % 2;
        }
    }
}

clear_cells() {
    int x,y;

    for(x=0; x<WIDTH/CELLPX; x++) {
        for(y=0; y<HEIGHT/CELLPX; y++) {
            cells[x][y] = 0;
        }
    }
}
